{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Using custom functions and tokenizers"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This notebook demonstrates how to use the `Partition` explainer for a multiclass text classification scenario where we are using a custom python function as our model."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"Using custom data configuration default\n",
"Reusing dataset emotion (/home/slundberg/.cache/huggingface/datasets/emotion/default/0.0.0/aa34462255cd487d04be8387a2d572588f6ceee23f784f37365aa714afeb8fe6)\n"
]
}
],
"source": [
"import datasets\n",
"import numpy as np\n",
"import pandas as pd\n",
"import scipy as sp\n",
"import torch\n",
"import transformers\n",
"\n",
"import shap\n",
"\n",
"# load the emotion dataset\n",
"dataset = datasets.load_dataset(\"emotion\", split=\"train\")\n",
"data = pd.DataFrame({\"text\": dataset[\"text\"], \"emotion\": dataset[\"label\"]})"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Define our model\n",
"\n",
"While here we are using the transformers package, any python function that takes in a list of strings and outputs scores will work."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"# load the model and tokenizer\n",
"tokenizer = transformers.AutoTokenizer.from_pretrained(\"nateraw/bert-base-uncased-emotion\", use_fast=True)\n",
"model = transformers.AutoModelForSequenceClassification.from_pretrained(\"nateraw/bert-base-uncased-emotion\").cuda()\n",
"labels = sorted(model.config.label2id, key=model.config.label2id.get)\n",
"\n",
"\n",
"# this defines an explicit python function that takes a list of strings and outputs scores for each class\n",
"def f(x):\n",
" tv = torch.tensor([tokenizer.encode(v, padding=\"max_length\", max_length=128, truncation=True) for v in x]).cuda()\n",
" attention_mask = (tv != 0).type(torch.int64).cuda()\n",
" outputs = model(tv, attention_mask=attention_mask)[0].detach().cpu().numpy()\n",
" scores = (np.exp(outputs).T / np.exp(outputs).sum(-1)).T\n",
" val = sp.special.logit(scores)\n",
" return val"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Create an explainer\n",
"\n",
"In order to build an `Explainer` we need both a model and a masker (the masker specifies how to hide portions of the input). Since we are using a custom function as our model, there is no way for SHAP to auto-infer a masker for us. So we need to provide one, either implicitly by passing a transformers tokenizer, or explicitly by building a `shap.maskers.Text` object"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"method = \"custom tokenizer\"\n",
"\n",
"# build an explainer by passing a transformers tokenizer\n",
"if method == \"transformers tokenizer\":\n",
" explainer = shap.Explainer(f, tokenizer, output_names=labels)\n",
"\n",
"# build an explainer by explicitly creating a masker\n",
"elif method == \"default masker\":\n",
" masker = shap.maskers.Text(r\"\\W\") # this will create a basic whitespace tokenizer\n",
" explainer = shap.Explainer(f, masker, output_names=labels)\n",
"\n",
"# build a fully custom tokenizer\n",
"elif method == \"custom tokenizer\":\n",
" import re\n",
"\n",
" def custom_tokenizer(s, return_offsets_mapping=True):\n",
" \"\"\"Custom tokenizers conform to a subset of the transformers API.\"\"\"\n",
" pos = 0\n",
" offset_ranges = []\n",
" input_ids = []\n",
" for m in re.finditer(r\"\\W\", s):\n",
" start, end = m.span(0)\n",
" offset_ranges.append((pos, start))\n",
" input_ids.append(s[pos:start])\n",
" pos = end\n",
" if pos != len(s):\n",
" offset_ranges.append((pos, len(s)))\n",
" input_ids.append(s[pos:])\n",
" out = {}\n",
" out[\"input_ids\"] = input_ids\n",
" if return_offsets_mapping:\n",
" out[\"offset_mapping\"] = offset_ranges\n",
" return out\n",
"\n",
" masker = shap.maskers.Text(custom_tokenizer)\n",
" explainer = shap.Explainer(f, masker, output_names=labels)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Compute SHAP values\n",
"\n",
"Explainers have the same method signature as the models they are explaining, so we just pass a list of strings for which to explain the classifications."
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"shap_values = explainer(data[\"text\"][:3])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Visualize the impact on all the output classes\n",
"\n",
"In the plots below, when you hover your mouse over an output class you get the explanation for that output class. When you click an output class name then that class remains the focus of the explanation visualization until you click another class.\n",
"\n",
"The base value is what the model outputs when the entire input text is masked, while $f_{output class}(inputs)$ is the output of the model for the full original input. The SHAP values explain in an addive way how the impact of unmasking each word changes the model output from the base value (where the entire input is masked) to the final prediction value."
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"scrolled": false
},
"outputs": [
{
"data": {
"text/html": [
"\n",
"
\n",
"